/*
 * Copyright 2007 Gergely Kis
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.gwthtml.servlet;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jdom.Document;

/**
 * Servlet implementation class for Servlet: HelloServlet
 *
 */
public class GWTServlet extends javax.servlet.http.HttpServlet implements
		javax.servlet.Servlet {
	/**
	 * 
	 */
	private static final long serialVersionUID = 5300698921176439084L;

	private static final String MODE_HTML = "html";

	private static final String MODE_NORMAL = "normal";
	
	
	private Map<String, String> mimeTypes = new HashMap<String, String>();

	/* (non-Java-doc)
	 * @see javax.servlet.http.HttpServlet#HttpServlet()
	 */
	public GWTServlet() {
		super();
	}

	/* (non-Java-doc)
	 * @see javax.servlet.http.HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		String mode = getRequestedMode(request);
		if (mode == null) {
			mode = getCurrentMode(request);
		}
		boolean modeChange = getCurrentMode(request) != mode;
		if (modeChange) {
			setCurrentMode(request, mode);
		}
		if (mode.equals(MODE_HTML)) {
			executeHtmlRequest(request, response, modeChange);
		} else {
			executeNormalRequest(request, response, modeChange);
		}
	}

	/* (non-Java-doc)
	 * @see javax.servlet.http.HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		String mode = getRequestedMode(request);
		if (mode == null) {
			mode = getCurrentMode(request);
		}
		boolean modeChange = getCurrentMode(request) != mode;
		if (modeChange) {
			setCurrentMode(request, mode);
		}
		if (mode.equals(MODE_HTML)) {
			executeHtmlRequest(request, response, modeChange);
		} else {
			executeNormalRequest(request, response, modeChange);
		}
	}

	private void executeHtmlRequest(HttpServletRequest request,
			HttpServletResponse response, boolean modeChange) {
		String path = request.getPathInfo();
		if (path == null || path.equals("") || path.equals("/")) {
		synchronized (request.getSession()) {
			if (modeChange) {
				GWTContext.clearContext(request, response); // Clear the context in case of a mode change
			}
			GWTContext.initContext(request, response, getServletContext());

			boolean requestDone = false;
			try {
				GWTContext.execute(this);
			} catch (GWTRequestDone gex) {
				requestDone = true;
			}

			GWTContext.commitContext(requestDone);
		}
		} else {
			// it is just the public path
			try {
				doGetPublicFile(request, response, getServletContext().getRealPath(path));
			} catch (IOException e) {
			}
		}
	}

	private String getRequestedMode(HttpServletRequest request) {
		String mode;
		if ((mode = request.getParameter("mode")) != null) {
			if (mode.equals(MODE_HTML) || mode.equals(MODE_NORMAL)) {
				return mode;
			}
		}
		return null;
	}

	private String getCurrentMode(HttpServletRequest request) {
		String mode = (String) request.getSession().getAttribute("GWT-Mode");
		if (mode == null) {
			mode = MODE_NORMAL;
		}
		return mode;
	}

	private void setCurrentMode(HttpServletRequest request, String mode) {
		request.getSession().setAttribute("GWT-Mode", mode);
	}

	private void executeNormalRequest(HttpServletRequest request,
			HttpServletResponse response, boolean modeChange) {
		String path = request.getPathInfo();
		if (path == null || path.equals("") || path.equals("/")) {
			// If the root is requested, then return the 
			Document descriptor = GWTContext.parseGWTModuleDescription(this,
					getServletConfig().getInitParameter("GWT-ModuleName"));
			path = GWTContext.getGWTPlaceholderPath(descriptor, this);			
		} else {
			path = getServletContext().getRealPath(path);
		}
		try {
			doGetPublicFile(request, response, path);
		} catch (IOException e) {
		}
	}

	private void doGetPublicFile(HttpServletRequest request,
			HttpServletResponse response,
			String path) throws IOException {


		URL foundResource;
		File requestedFile = new File(path);
		if (requestedFile.exists()) {
			try {
				foundResource = requestedFile.toURL();
			} catch (MalformedURLException e) {
				sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
						"Malformed URL Exception for'" + path + "' in the public path'");
				return;				
			}
		} else {
			sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
					"Cannot find resource '" + path + "' in the public path'");
			return;
		}

		// Get the MIME type.
		String extPath = foundResource.toExternalForm();
		String mimeType = null;
		try {
			mimeType = getServletContext().getMimeType(extPath);
		} catch (UnsupportedOperationException e) {
			// Certain minimalist servlet containers throw this.
			// Fall through to guess the type.
		}

		if (mimeType == null) {
			mimeType = guessMimeType(path);
		}

		// Maybe serve it up. Don't let the client cache anything other than
		// xxx.cache.yyy files because this servlet is for development (so user
		// files are assumed to change a lot), although we do honor
		// "If-Modified-Since".

		boolean infinitelyCacheable = isInfinitelyCacheable(path);

		InputStream is = null;
		try {
			// Check for up-to-datedness.
			URLConnection conn = foundResource.openConnection();
			long lastModified = conn.getLastModified();
			if (isNotModified(request, lastModified)) {
				response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
				if (infinitelyCacheable) {
					response.setHeader(HttpHeaders.CACHE_CONTROL,
							HttpHeaders.CACHE_CONTROL_MAXAGE_FOREVER);
				}
				return;
			}

			// Set up headers to really send it.
			response.setStatus(HttpServletResponse.SC_OK);
			long now = new Date().getTime();
			response.setHeader(HttpHeaders.DATE, HttpHeaders
					.toInternetDateFormat(now));
			response.setContentType(mimeType);
			String lastModifiedStr = HttpHeaders
					.toInternetDateFormat(lastModified);
			response.setHeader(HttpHeaders.LAST_MODIFIED, lastModifiedStr);

			// Expiration header. Either immediately stale (requiring an
			// "If-Modified-Since") or infinitely cacheable (not requiring even a
			// freshness check).
			String maxAgeStr;
			if (infinitelyCacheable) {
				maxAgeStr = HttpHeaders.CACHE_CONTROL_MAXAGE_FOREVER;
			} else {
				maxAgeStr = HttpHeaders.CACHE_CONTROL_MAXAGE_EXPIRED;
			}
			response.setHeader(HttpHeaders.CACHE_CONTROL, maxAgeStr);

			// Content length.
			int contentLength = conn.getContentLength();
			if (contentLength >= 0) {
				response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer
						.toString(contentLength));
			}

			// Send the bytes.
			is = foundResource.openStream();
			streamOut(is, response.getOutputStream(), 1024 * 8);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException swallowed) {
					// Nothing we can do now.
					//
				}
			}
		}
	}

	private String guessMimeType(String fullPath) {
		int dot = fullPath.lastIndexOf('.');
		if (dot != -1) {
			String ext = fullPath.substring(dot + 1);
			String mimeType = (String) mimeTypes.get(ext);
			if (mimeType != null) {
				return mimeType;
			}

			// Otherwise, fall through.
			//
		}

		// Last resort.
		//
		return "application/octet-stream";
	}

	private void initMimeTypes() {
		mimeTypes.put("abs", "audio/x-mpeg");
		mimeTypes.put("ai", "application/postscript");
		mimeTypes.put("aif", "audio/x-aiff");
		mimeTypes.put("aifc", "audio/x-aiff");
		mimeTypes.put("aiff", "audio/x-aiff");
		mimeTypes.put("aim", "application/x-aim");
		mimeTypes.put("art", "image/x-jg");
		mimeTypes.put("asf", "video/x-ms-asf");
		mimeTypes.put("asx", "video/x-ms-asf");
		mimeTypes.put("au", "audio/basic");
		mimeTypes.put("avi", "video/x-msvideo");
		mimeTypes.put("avx", "video/x-rad-screenplay");
		mimeTypes.put("bcpio", "application/x-bcpio");
		mimeTypes.put("bin", "application/octet-stream");
		mimeTypes.put("bmp", "image/bmp");
		mimeTypes.put("body", "text/html");
		mimeTypes.put("cdf", "application/x-cdf");
		mimeTypes.put("cer", "application/x-x509-ca-cert");
		mimeTypes.put("class", "application/java");
		mimeTypes.put("cpio", "application/x-cpio");
		mimeTypes.put("csh", "application/x-csh");
		mimeTypes.put("css", "text/css");
		mimeTypes.put("dib", "image/bmp");
		mimeTypes.put("doc", "application/msword");
		mimeTypes.put("dtd", "text/plain");
		mimeTypes.put("dv", "video/x-dv");
		mimeTypes.put("dvi", "application/x-dvi");
		mimeTypes.put("eps", "application/postscript");
		mimeTypes.put("etx", "text/x-setext");
		mimeTypes.put("exe", "application/octet-stream");
		mimeTypes.put("gif", "image/gif");
		mimeTypes.put("gtar", "application/x-gtar");
		mimeTypes.put("gz", "application/x-gzip");
		mimeTypes.put("hdf", "application/x-hdf");
		mimeTypes.put("hqx", "application/mac-binhex40");
		mimeTypes.put("htc", "text/x-component");
		mimeTypes.put("htm", "text/html");
		mimeTypes.put("html", "text/html");
		mimeTypes.put("hqx", "application/mac-binhex40");
		mimeTypes.put("ief", "image/ief");
		mimeTypes.put("jad", "text/vnd.sun.j2me.app-descriptor");
		mimeTypes.put("jar", "application/java-archive");
		mimeTypes.put("java", "text/plain");
		mimeTypes.put("jnlp", "application/x-java-jnlp-file");
		mimeTypes.put("jpe", "image/jpeg");
		mimeTypes.put("jpeg", "image/jpeg");
		mimeTypes.put("jpg", "image/jpeg");
		mimeTypes.put("js", "text/javascript");
		mimeTypes.put("jsf", "text/plain");
		mimeTypes.put("jspf", "text/plain");
		mimeTypes.put("kar", "audio/x-midi");
		mimeTypes.put("latex", "application/x-latex");
		mimeTypes.put("m3u", "audio/x-mpegurl");
		mimeTypes.put("mac", "image/x-macpaint");
		mimeTypes.put("man", "application/x-troff-man");
		mimeTypes.put("me", "application/x-troff-me");
		mimeTypes.put("mid", "audio/x-midi");
		mimeTypes.put("midi", "audio/x-midi");
		mimeTypes.put("mif", "application/x-mif");
		mimeTypes.put("mov", "video/quicktime");
		mimeTypes.put("movie", "video/x-sgi-movie");
		mimeTypes.put("mp1", "audio/x-mpeg");
		mimeTypes.put("mp2", "audio/x-mpeg");
		mimeTypes.put("mp3", "audio/x-mpeg");
		mimeTypes.put("mpa", "audio/x-mpeg");
		mimeTypes.put("mpe", "video/mpeg");
		mimeTypes.put("mpeg", "video/mpeg");
		mimeTypes.put("mpega", "audio/x-mpeg");
		mimeTypes.put("mpg", "video/mpeg");
		mimeTypes.put("mpv2", "video/mpeg2");
		mimeTypes.put("ms", "application/x-wais-source");
		mimeTypes.put("nc", "application/x-netcdf");
		mimeTypes.put("oda", "application/oda");
		mimeTypes.put("pbm", "image/x-portable-bitmap");
		mimeTypes.put("pct", "image/pict");
		mimeTypes.put("pdf", "application/pdf");
		mimeTypes.put("pgm", "image/x-portable-graymap");
		mimeTypes.put("pic", "image/pict");
		mimeTypes.put("pict", "image/pict");
		mimeTypes.put("pls", "audio/x-scpls");
		mimeTypes.put("png", "image/png");
		mimeTypes.put("pnm", "image/x-portable-anymap");
		mimeTypes.put("pnt", "image/x-macpaint");
		mimeTypes.put("ppm", "image/x-portable-pixmap");
		mimeTypes.put("ppt", "application/powerpoint");
		mimeTypes.put("ps", "application/postscript");
		mimeTypes.put("psd", "image/x-photoshop");
		mimeTypes.put("qt", "video/quicktime");
		mimeTypes.put("qti", "image/x-quicktime");
		mimeTypes.put("qtif", "image/x-quicktime");
		mimeTypes.put("ras", "image/x-cmu-raster");
		mimeTypes.put("rgb", "image/x-rgb");
		mimeTypes.put("rm", "application/vnd.rn-realmedia");
		mimeTypes.put("roff", "application/x-troff");
		mimeTypes.put("rtf", "application/rtf");
		mimeTypes.put("rtx", "text/richtext");
		mimeTypes.put("sh", "application/x-sh");
		mimeTypes.put("shar", "application/x-shar");
		mimeTypes.put("smf", "audio/x-midi");
		mimeTypes.put("sit", "application/x-stuffit");
		mimeTypes.put("snd", "audio/basic");
		mimeTypes.put("src", "application/x-wais-source");
		mimeTypes.put("sv4cpio", "application/x-sv4cpio");
		mimeTypes.put("sv4crc", "application/x-sv4crc");
		mimeTypes.put("swf", "application/x-shockwave-flash");
		mimeTypes.put("t", "application/x-troff");
		mimeTypes.put("tar", "application/x-tar");
		mimeTypes.put("tcl", "application/x-tcl");
		mimeTypes.put("tex", "application/x-tex");
		mimeTypes.put("texi", "application/x-texinfo");
		mimeTypes.put("texinfo", "application/x-texinfo");
		mimeTypes.put("tif", "image/tiff");
		mimeTypes.put("tiff", "image/tiff");
		mimeTypes.put("tr", "application/x-troff");
		mimeTypes.put("tsv", "text/tab-separated-values");
		mimeTypes.put("txt", "text/plain");
		mimeTypes.put("ulw", "audio/basic");
		mimeTypes.put("ustar", "application/x-ustar");
		mimeTypes.put("xbm", "image/x-xbitmap");
		mimeTypes.put("xht", "application/xhtml+xml");
		mimeTypes.put("xhtml", "application/xhtml+xml");
		mimeTypes.put("xml", "text/xml");
		mimeTypes.put("xpm", "image/x-xpixmap");
		mimeTypes.put("xsl", "text/xml");
		mimeTypes.put("xwd", "image/x-xwindowdump");
		mimeTypes.put("wav", "audio/x-wav");
		mimeTypes.put("svg", "image/svg+xml");
		mimeTypes.put("svgz", "image/svg+xml");
		mimeTypes.put("vsd", "application/x-visio");
		mimeTypes.put("wbmp", "image/vnd.wap.wbmp");
		mimeTypes.put("wml", "text/vnd.wap.wml");
		mimeTypes.put("wmlc", "application/vnd.wap.wmlc");
		mimeTypes.put("wmls", "text/vnd.wap.wmlscript");
		mimeTypes.put("wmlscriptc", "application/vnd.wap.wmlscriptc");
		mimeTypes.put("wrl", "x-world/x-vrml");
		mimeTypes.put("Z", "application/x-compress");
		mimeTypes.put("z", "application/x-compress");
		mimeTypes.put("zip", "application/zip");
	}

	/**
	 * A file is infinitely cacheable if it ends with ".cache.xxx", where "xxx"
	 * can be any extension.
	 */
	private boolean isInfinitelyCacheable(String path) {
		int lastDot = path.lastIndexOf('.');
		if (lastDot >= 0) {
			if (path.substring(0, lastDot).endsWith(".cache")) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks to see whether or not a client's file is out of date relative to the
	 * original.
	 */
	private boolean isNotModified(HttpServletRequest request,
			long ageOfServerCopy) {
		// The age of the server copy *must* have the milliseconds truncated.
		// Since milliseconds isn't part of the GMT format, failure to truncate
		// will leave the file in a state where it appears constantly out of date
		// and yet it can never get in sync because the Last-Modified date keeps
		// truncating off the milliseconds part on its way out.
		// 
		ageOfServerCopy -= (ageOfServerCopy % 1000);

		long ageOfClientCopy = 0;
		String ifModifiedSince = request.getHeader("If-Modified-Since");
		if (ifModifiedSince != null) {
			// Rip off any additional stuff at the end, such as "; length="
			// (IE does add this).
			//
			int lastSemi = ifModifiedSince.lastIndexOf(';');
			if (lastSemi != -1) {
				ifModifiedSince = ifModifiedSince.substring(0, lastSemi);
			}
			ageOfClientCopy = HttpHeaders
					.fromInternetDateFormat(ifModifiedSince);
		}

		if (ageOfClientCopy >= ageOfServerCopy) {
			// The client already has a good copy.
			//
			return true;
		} else {
			// The client needs a fresh copy of the requested file.
			//
			return false;
		}
	}

	private void sendErrorResponse(HttpServletResponse response,
			int statusCode, String msg) throws IOException {
		response.setContentType("text/html");
		response.getWriter().println(msg);
		response.setStatus(statusCode);
	}

	private void streamOut(InputStream in, OutputStream out, int bufferSize)
			throws IOException {
		assert (bufferSize >= 0);

		byte[] buffer = new byte[bufferSize];
		int bytesRead = 0;
		while (true) {
			bytesRead = in.read(buffer);
			if (bytesRead >= 0) {
				// Copy the bytes out.
				out.write(buffer, 0, bytesRead);
			} else {
				// End of input stream.
				return;
			}
		}
	}

}